/*
 * lcd.c
 * 
 * Copyright 2012 Kamil Cukrowski <kamil@dyzio.pl>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 * 
 */

#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <libusb-1.0/libusb.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>

#include "uhc_config.h"
#include "uhc_cgram_default.h"
#include "hd44780-inst-def.h"
#include "hd44780_charmap.h"
#include "cgram_default.h"
#include "lcd.h"

#define UHC_DEBUG(smt, ...) //printf("LCD: %s %s %d " smt, __FILE__, __FUNCTION__, __LINE__, ## __VA_ARGS__)
#define CLOCK_DEBUG(...) //printf( __VA_ARGS__)

#define PINFO(...) printf( __VA_ARGS__ );
#define PDEBUG(str, ...) PDEBUGL(1, str, ##__VA_ARGS__);
#define PDEBUGL(x, str, ...) do{ if ( debug >= x ) _INTERNAL_LOGIT(str, ##__VA_ARGS__); }while(0)
#define PERROR(str, ...) do { printf("msguhc: (%s:%s:%d): ", __FILE__, __FUNCTION__,  __LINE__); printf(str, ##__VA_ARGS__); printf("error %d:%s \n", errno, strerror(errno)); }while(0)
#define _INTERNAL_LOGIT(str, ...) do { printf("msguhc: (%s:%s:%d): ", __FILE__, __FUNCTION__, __LINE__); printf(str, ##__VA_ARGS__); printf("\n"); }while(0)

extern int debug;
extern int timer_sec;
extern unsigned char lcd_rows;
/* libusb */
libusb_context *ctx = NULL;
struct libusb_device_handle *handle;
/* state */
unsigned char state[MAX_DISP_ROWS][DISP_COLS];
/* pthread*/
pthread_t thread;
pthread_mutex_t lock;
static pthread_cond_t thread_signal;
unsigned int thread_cancel = 0;

/* --------------------------------------------------------------- */

struct libusb_device_handle *usb_connect(libusb_context *ctx, int vendor, int product)
{
	struct libusb_device_handle *handle;
	int result;
	int restore_kernel_driver = 0;

	libusb_set_debug(ctx, 3);

	/* Detect device */
	handle = libusb_open_device_with_vid_pid(ctx, vendor, product);
	if (handle == NULL) {
		printf("Cannot find device %hX, %hX\n", vendor, product);
		goto clean_context;
	}

	result = libusb_kernel_driver_active(handle, 0);
	printf(" libusb_kernel_driver_active %d \n", result);
	if (result < 0) {
		printf("Error %i checking kernel driver\n", result);
		goto clean_handle;
	}
	if (result > 1) {
		result = libusb_detach_kernel_driver(handle, 0);
		if (result < 0) {
			printf("Error %i detaching kernel driver\n", result);
			goto clean_handle;
		}
		restore_kernel_driver = 1;
	}

	result = libusb_claim_interface(handle, 0);
	if (result < 0) {
		printf("Error %i claiming interface: %s \n", result, libusb_strerror(result) );
		goto restore_kernel_driver;
	}

	return handle;
	
//release_interface:
	libusb_release_interface(handle, 0);

 restore_kernel_driver:
	if (restore_kernel_driver)
		libusb_attach_kernel_driver(handle, 0);

 clean_handle:
	libusb_close(handle);

 clean_context:
	//libusb_exit(ctx);
	
 //quit:
	return NULL;
}

struct libusb_device_handle *usb_disconnect(libusb_context *ctx, struct libusb_device_handle *handle)
{
	libusb_release_interface(handle, 0);
	libusb_close(handle);
	return NULL;
}

/* --------------------------------------------------------------- */

static int uhc_send(unsigned char *buff, int size)
{
	int len;
	int ret;
	
	if ( !handle ) return -1;
	if ( size == 0 ) return -1;
	if ( !buff ) return -1;
	
	ret = libusb_bulk_transfer(handle, 0x01, buff, size, &len, 1000);
	switch(ret) {
	case 0:
		//printf(" sended %d butes: %s  \n", len, buff);
		//UHC_DEBUG(" sended %d bytes \n", len);
		break;
	case LIBUSB_ERROR_TIMEOUT:
		printf(" LIBUSB_ERROR_TIMEOUT \n");
		break;
	case LIBUSB_ERROR_PIPE:
		printf(" LIBUSB_ERROR_PIPE \n");
		break;
	case LIBUSB_ERROR_NO_DEVICE:
		printf(" LIBUSB_ERROR_NO_DEVICE  \n");
		break;
	case LIBUSB_ERROR_OVERFLOW:
		printf("LIBUSB_ERROR_OVERFLOW \n");
		break;
	default:
		printf(" some funckgin error \n");
	}
	switch(ret) {
	case LIBUSB_ERROR_NO_DEVICE:
	case LIBUSB_ERROR_PIPE:
		handle = usb_disconnect(ctx, handle);
	}
	
	return ret;
}

static int _uhc_receive(unsigned char *buff, int *size, int timeout)
{
	int ret;
	int len;
	
	if ( !handle ) return -1;
	if ( *size == 0 ) return -1;
	if ( !buff ) return -1;
	/**
		dev_handle	a handle for the device to communicate with
		endpoint	the address of a valid endpoint to communicate with
		data	a suitably-sized data buffer for either input or output (depending on endpoint)
		length	for bulk writes, the number of bytes from data to be sent. for bulk reads, the maximum number of bytes to receive into the data buffer.
		transferred	output location for the number of bytes actually transferred.
		timeout	timeout (in millseconds) that this function should wait before giving up due to no response being received. For an unlimited timeout, use value 0.
	* */
	ret = libusb_bulk_transfer(handle, 0x82, buff, *size, &len, timeout);
	switch(ret) {
	case 0:
		PDEBUGL(6, " received %d bytes ", len);
		break;
	case LIBUSB_ERROR_TIMEOUT:
		PDEBUGL(6," LIBUSB_ERROR_TIMEOUT");
		break;
	case LIBUSB_ERROR_PIPE:
		printf(" LIBUSB_ERROR_PIPE \n");
		break;
	case LIBUSB_ERROR_NO_DEVICE:
		printf(" LIBUSB_ERROR_NO_DEVICE  \n");
		break;
	case LIBUSB_ERROR_OVERFLOW:
		printf("LIBUSB_ERROR_OVERFLOW \n");
		break;
	default:
		printf(" some funckgin error \n");
	}
	switch(ret) {
	case LIBUSB_ERROR_NO_DEVICE:
	case LIBUSB_ERROR_PIPE:
		handle = usb_disconnect(ctx, handle);
	}
	
	*size = len;
	return ret;
}

static int uhc_receive(unsigned char *buff, int *size)
{
	return _uhc_receive(buff, size, 1000);
}

/* --------------------------------------------------------------- */

static void check_errors() 
{
	int i;
	unsigned char buff[256];
	int ret;
	int len;
	int j;
	
	PDEBUGL(3, "check_errors checking screens!");
	
	if ( !handle ) return;
	
	for(i=0;i<lcd_rows;i++) {
		const unsigned char ctrl = i/ROWS_PER_CTRL;
		const unsigned char row = i%ROWS_PER_CTRL;
		
		
		// set cursor pos to ctrl_row and beggining of the line and send write request for 40 chars.
		buff[0] = CAKE_FILLING_RAW | (ctrl<<2) | RS_INST | RW_WRITE;
		buff[1] = HD44780_DDRAM_ADDRESS | ( row << 6 ) | 0;
		buff[2] = CAKE_FILLING_NUM_READ | ctrl<<2 | RW_READ | RS_DATA;
		buff[3] = 40;
		buff[4] = CAKE_FILLING_SET_READ;
		
		ret = uhc_send(buff, 5);
		if ( ret ) {
			PDEBUG(" uhc_send error %d \n", ret );
			return;
		}
		
		len = DISP_COLS;
		ret = uhc_receive(buff, &len);
		if ( ret ) {
			PDEBUG(" usb_receive error %d\n", ret);
			return;
		}
		
		if ( len != DISP_COLS ) {
			PDEBUG(" error in checking, len != DISP_COLS \n ");
			return;
		}
		if ( memcmp(state[i], buff, DISP_COLS*sizeof(char)) ) {
				PDEBUG(" ERROR IN ROW:%d should be '%s' \n\t\t and is '%s' correnting \n", 
					i, state[i], buff);
				LCD_print_line(i, (char *)state[i]);
		}
		if ( debug >= 7 ) {
			printf(" check_errors: ctrl %d row %d : received: %d |", ctrl, row, len);
			for(j=0;j<len;j++)
				printf("%c", buff[j]);
			printf("|\n");
		}
	}
}


/* pthread for checking errors */

static void *thread_work(void *arg)
{
	int ret;
	struct timespec ts;
	for(;;) {
		clock_gettime(CLOCK_REALTIME, &ts);
		ts.tv_sec += timer_sec;
		if ( thread_cancel ) break;
		ret = pthread_cond_timedwait(&thread_signal, &lock, &ts);
		if ( thread_cancel ) break;
		check_errors();
		pthread_mutex_unlock(&lock);
	}
	return NULL;
}

/* --------------------------------------------------------------- */

static int LCD_get_config()
{
	unsigned char buff[64];
	unsigned char temp[9];
	int size;
	int i;
	int ret;
	
	pthread_mutex_lock(&lock);
	
	// discard any data
	do {
		size = 64;
		i = _uhc_receive(buff, &size, 100);
		if ( i == LIBUSB_ERROR_TIMEOUT )
			break;
		else if ( i < 0 )
			return -800;
	} while( size != 0 );
	
	PDEBUGL(3, "LCD_GET_CONFIG ");
	
	buff[0] = CAKE_FILLING_GET_CONFIG;
	buff[1] = CAKE_FILLING_SET_READ;
	ret = uhc_send(buff, 2);
	if ( ret < 0 ) {
		PDEBUGL(3, " tutaj -1 ret=%d ", ret);
		return -1;
	}
	usleep(100); /* give time for uhcpic to arm the endpoint */
	size = 64;
	ret = uhc_receive(buff, &size);
	if ( ret < 0 ) {
		PDEBUGL(3, " tutaj -2 ret=%d ", ret);
		return -2;
	}
	
	// from specyfication
//	const int _disp_cols = buff[0];
//	const int _disp_rows = buff[1];
// const int _num_controllers = buff[2];
//	const int _rows_per_ctrl = buff[3];
	const int sizeoflcddev = buff[4];
	
	
	for(i=0;i<8;i++) {
		temp[i] = (~buff[5])&1<<i ? '1' : '0';
	}
	temp[8] = 0;
	
	for(i=0;i<8;i++) {
		if ( buff[5]&1<<i )
			break;
	}
	lcd_rows = i*ROWS_PER_CTRL;
	if ( lcd_rows > MAX_DISP_ROWS ) 
		lcd_rows = MAX_DISP_ROWS;
	printf("REGISTEREED %d lcd_rpws \n", lcd_rows );
	
	
	PDEBUGL(2, 
		"pic specification: \n"
		" ^ disp_cols: %d \n"
		" ^ disp_rows: %d \n"
		" ^ num_controllers: %d \n"
		" ^ rows_per_ctrl: %d \n"
		" ^ sizeof(lcddev): %d \n"
		" ^ lcddev.disabled: %x %8s\n"
		, buff[0], buff[1], buff[2], buff[3], buff[4], buff[5], temp);
	
	
	/* read lcddev struct */
	buff[0] = CAKE_FILLING_GET_INFO;
	buff[1] = CAKE_FILLING_SET_READ;
	ret = uhc_send(buff, 2);
	if ( ret < 0 ) {
		PDEBUGL(3, " tutaj ret=%d ", ret);
		return -4;
	}
	usleep(100); /* give time for uhcpic to arm the endpoint */
	size = 64;
	ret = uhc_receive(buff, &size);
	if ( ret < 0 ) {
		PDEBUGL(3, " tutaj ret=%d ", ret);
		return -5;
	}
	
	PDEBUGL(2, "lcddev struct has:");
	for(i=0;i<sizeoflcddev; i++)
		PDEBUGL(2, " ^ bajt[%d] : %x", i, buff[i]);
	PDEBUGL(2, "--- end of information");
	
	pthread_mutex_unlock(&lock);
	
	return 0;
}

/** LCD_write_CGRAM
 * ctrl_nr - controller number, or if ctrl_nr == -1 then writing to alll ! 
 */
static int LCD_write_CGRAM(int ctrl_nr, unsigned char cgram_index, unsigned char *cgram_pixels)
{
	unsigned char i;
	unsigned char buff[250];
	
	if ( ctrl_nr < 0 )
		buff[0] = CAKE_FILLING_ALL_RAW | RS_INST | RW_WRITE;
	else
		buff[0] = CAKE_FILLING_RAW | ctrl_nr<<2 | RS_INST | RW_WRITE;
	buff[1] = HD44780_CGRAM_ADDRESS | ( cgram_index<<3 ); // x<<3 is faster then x*8
	for (i = 0; i < 8; i++ ) {
		
		if ( ctrl_nr < 0 ) 
			buff[2+i*2+0] = CAKE_FILLING_ALL_RAW | RS_DATA | RW_WRITE;
		else
			buff[2+i*2+0] = CAKE_FILLING_RAW | ctrl_nr<<2 | RS_DATA | RW_WRITE;
		buff[2+i*2+1] = cgram_pixels[i];
		
	}
	
	return uhc_send(buff, 2+16);
}

static int LCD_load_default_cgram()
{
	int i;
	unsigned char cg[8][8] = DEFAULT_CGRAM;
	for(i=0;i<8;i++) {
		if ( LCD_write_CGRAM(-1, i, cg[i]) < 0 )
			return -i-1;
	}
	return 0;
}

/* -------------------------------------------------------------- */

/* static void tlumacz(char *str, int str_len)
{
	const char polskie_znaki[] = "ąćęłńóśźżĄĆĘŁŃÓŚŹŻ";
	const char polskie_znaki_change[] = "acelnoszzACELNOSZZ";
	const int polskie_znaki_len = strlen(polskie_znaki);	
	int i, j, k;
	
	for(i=0;i<str_len-1;i++) {
		for(j=0;j<polskie_znaki_len/2;j++) {
			if ( str[i] == polskie_znaki[j*2] ) {
				if ( str[i+1] == polskie_znaki[j*2+1] ) {
					str[i] = polskie_znaki_change[j];
					for(k=i+1;k<str_len;k++) {
						str[k] = str[k+1];
					}
				}
			}
		}
	}
} */

static int LCD_send_inst_all(char config)
{
	unsigned char buff[2] = {
		[0] = CAKE_FILLING_ALL_RAW | RS_INST | RW_WRITE,
		[1] = config,
	};
	return uhc_send(buff, 2);
}

int LCD_rst()
{
	int ret;
	unsigned char buff[2] = {
		[0] = CAKE_FILLING_ALL_RAW | RS_INST | RW_WRITE,
		[1] = HD44780_CLRDISP,
		
	};
	if ( !handle ) return -1;
	pthread_mutex_lock(&lock);
	memset(state, ' ', sizeof(char)*lcd_rows*DISP_COLS);
	ret = uhc_send(buff, 2);
	pthread_mutex_unlock(&lock);
	return ret;
}

int LCD_print(const char *text, const int size)
{
	int ret;
	if ( !handle ) return -1;
	pthread_mutex_lock(&lock);
	ret = uhc_send((unsigned char *)text, size);
	pthread_mutex_unlock(&lock);
	return ret;
}

static void _LCD_print_line_fill(unsigned char *fill, const unsigned char fill_byte, const unsigned char *text)
{
	// text is 40 char long
	// fill is at least 80 char long !! (not chacking !)
	int i=0;
	while(i<80) {
		fill[i] = fill_byte;
		i++;
		fill[i] = *(text++);
		i++;
	}
}

int LCD_print_line(const unsigned char row, const char const *text)
{
	const int col = 0;
	const unsigned char ctrl = row / ROWS_PER_CTRL;
	unsigned int i;
	int ret;
	unsigned char buff[DISP_COLS*2+2] = {
		[0] = CAKE_FILLING_RAW | (ctrl<<2) | RS_INST | RW_WRITE,
		[1] = HD44780_DDRAM_ADDRESS | ( row << 6 ) | col,
	};
	
	PDEBUGL(3, " %s: %d \"%s\" ", __FUNCTION__, row, text);
	
	if ( !handle ) return -1;
	pthread_mutex_lock(&lock);
	_LCD_print_line_fill(buff+2, CAKE_FILLING_RAW | (ctrl<<2) | RS_DATA | RW_WRITE, (unsigned char *)text);
	for(i=0;i<DISP_COLS;i++)
		state[row][i] = (unsigned char)text[i];
	ret = uhc_send(buff, DISP_COLS*2+2);
	pthread_mutex_unlock(&lock);
	return ret;
}

int LCD_clr_line(const unsigned int row)
{
	unsigned char empty[40] = { [0 ... 39] = ' ' };
	/* mutexes handled in LCD_print_line */
	return LCD_print_line(row, (char *)empty);
}

int LCD_ok()
{
	return (handle != NULL);
}

int LCD_init()
{
	int ret;
	
	if ( !ctx ) {
		ret = libusb_init(&ctx);
		if (ret < 0) {
			printf("Error %i initializing usb context\n", ret);
			return -100;
		}
	}
	
	ret = pthread_mutex_init(&lock, NULL);
	if (ret != 0)
	{
		PDEBUG("pthread_mutex_init ret=%d\n", ret);
		return ret;
	}
	
	handle = usb_connect(ctx, UHCMOD_USB_VENDOR_ID, UHCMOD_USB_PRODUCT_ID);
	if ( !handle ) {
		printf(" not conected \n");
		return -1;
	}
	PINFO(" Connected to device !\n");
	
	/* no care about mutex */
	
	if ( LCD_send_inst_all(HD44780_DOOC_DISPLAYON|HD44780_DOOC_CURSOROFF|HD44780_DOOC_CURSORNOBLINK) < 0 ) {
		handle = usb_disconnect(ctx, handle);
		return -8;
	}
	
	if ( LCD_rst() < 0 ) {
		handle = usb_disconnect(ctx, handle);
		return -2;
	}
	
	if ( LCD_load_default_cgram() < 0 ) {
		handle = usb_disconnect(ctx, handle);
		return -3;
	}
	
	ret = LCD_get_config();
	if ( ret < 0 ){
		handle = usb_disconnect(ctx, handle);
		return ret<<4;
	}
	
	memset(state, ' ', sizeof(char)*MAX_DISP_ROWS*DISP_COLS);
	
	if ( timer_sec > 0 ) {
		ret = pthread_cond_init(&thread_signal, NULL);
		if (ret != 0) {
			printf("pthread_cond_init ret=%d\n", ret);
			return ret;
		}
		ret = pthread_create(&thread, NULL, &thread_work, NULL);
		if (ret != 0) {
			PDEBUG("pthread_create ret=%d strerorr(ret)=%s", ret, strerror(ret));
			return ret;
		}
	}
	
	return 0;
}

void LCD_close()
{
	if ( timer_sec > 0 ) {
		thread_cancel = 1;
		pthread_cond_signal(&thread_signal);
		pthread_join(thread, NULL);
		pthread_mutex_unlock(&lock);
		pthread_cond_destroy(&thread_signal);
	}
	if ( !handle ) goto MUTEX_DESTROY;
	LCD_rst();
	LCD_print_line(1, "            Dobranoc Kamil :D           ");
	handle = usb_disconnect(ctx, handle);
	libusb_exit(ctx);
MUTEX_DESTROY:
	pthread_mutex_destroy(&lock);
}
